MultiOutputRegressor can't process estimators with synchronization primitives

### Describe the bug

[MultiOutputRegressor ](https://scikit-learn.org/stable/modules/generated/sklearn.multioutput.MultiOutputRegressor.html) can't process estimators with threading/multiprocessing synchronization primitives

I want to propagate stop_event to the callback of regressor. I think the issue is because MultiOutputRegressor is trying to pickle each of estimator to move it to another thread/process. And if the estimator contains any synchronization primitives - they can't be pickled, so it fails. Maybe the solution might be to allow to provide pre-created estimators (for each of output) and provide them to the init of MultiOutputRegressor.

I need to use MultiOutputRegressor because I need to export XGBoost model into onnx with a help of [skl2onnx](https://onnx.ai/sklearn-onnx/). If I don't use MultiOutputRegressor  - skl2onnx doesn't allow me to export, despite XGBoost has an [experimental way of multiple outputs](https://xgboost.readthedocs.io/en/stable/tutorials/multioutput.html).

Or maybe I missed something. Please help.


Packages:

```
xgboost                   3.0.0
```

### Steps/Code to Reproduce

```python
from threading import Event

from sklearn.model_selection import train_test_split
from sklearn.datasets import make_regression
from sklearn.multioutput import MultiOutputRegressor

from xgboost import XGBRegressor, Booster
from xgboost import callback as xgb_callbacks


class Callback(xgb_callbacks.TrainingCallback):
    def __init__(self, stop_event: Event):
        super().__init__()
        self.stop_event = stop_event

    def after_iteration(self, model: Booster, epoch: int, evals_log: dict[str, dict]) -> bool:
        print(f"xgboost training: epoch {epoch}, evals_log {evals_log}")
        return False


def train_xgboost(X_train, y_train):
    stop_event = Event()

    base_model = XGBRegressor(n_estimators=45, callbacks=[Callback(stop_event)])
    model = MultiOutputRegressor(base_model)
    # base_model.callbacks = [Callback(stop_event)]

    eval_set = []
    for i in range(y_train.shape[1]):
        y = y_train[:, i]
        eval_set.append((X_train, y))

    model.fit(X=X_train, y=y_train, eval_set=eval_set, verbose=True)

    return model


def get_data():
    X, y = make_regression(n_samples=100,
                           n_features=5,
                           n_informative=2,
                           n_targets=3,
                           noise=10.0,
                           random_state=42)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
    return X_train, y_train, X_test, y_test

def main():
    X_train, y_train, X_test, y_test = get_data()
    ml_model = train_xgboost(X_train, y_train)


if __name__ == "__main__":
    main()
```

### Expected Results

No errors

### Actual Results

```
Traceback (most recent call last):
  File "D:\my_test\issue_pickl.py", line 54, in <module>
    main()
  File "D:\my_test\issue_pickl.py", line 50, in main
    ml_model = train_xgboost(X_train, y_train)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\issue_pickl.py", line 33, in train_xgboost
    model.fit(X=X_train, y=y_train, eval_set=eval_set, verbose=True)
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 1389, in wrapper
    return fit_method(estimator, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\multioutput.py", line 274, in fit
    self.estimators_ = Parallel(n_jobs=self.n_jobs)(
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\utils\parallel.py", line 77, in __call__
    return super().__call__(iterable_with_config)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\joblib\parallel.py", line 1918, in __call__
    return output if self.return_generator else list(output)
                                                ^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\joblib\parallel.py", line 1847, in _get_sequential_output
    res = func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\utils\parallel.py", line 139, in __call__
    return self.function(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\multioutput.py", line 59, in _fit_estimator
    estimator = clone(estimator)
                ^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 94, in clone
    return estimator.__sklearn_clone__()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 300, in __sklearn_clone__
    return _clone_parametrized(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 127, in _clone_parametrized
    new_object_params[name] = clone(param, safe=False)
                              ^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 95, in clone
    return _clone_parametrized(estimator, safe=safe)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 105, in _clone_parametrized
    return estimator_type([clone(e, safe=safe) for e in estimator])
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 105, in <listcomp>
    return estimator_type([clone(e, safe=safe) for e in estimator])
                           ^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 95, in clone
    return _clone_parametrized(estimator, safe=safe)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 108, in _clone_parametrized
    return copy.deepcopy(estimator)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 161, in deepcopy
    rv = reductor(4)
         ^^^^^^^^^^^
TypeError: cannot pickle '_thread.lock' object
```

### Versions

```shell
System:
    python: 3.11.9 (tags/v3.11.9:de54cf5, Apr  2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)]
executable: D:\my_test\sys\win64\python\python.exe
   machine: Windows-10-10.0.22631-SP0

Python dependencies:
      sklearn: 1.6.1
          pip: 25.0.1
   setuptools: 65.5.0
        numpy: 1.26.4
        scipy: 1.15.2
       Cython: None
       pandas: 2.2.3
   matplotlib: 3.10.1
       joblib: 1.4.2
threadpoolctl: 3.6.0

Built with OpenMP: True

threadpoolctl info:
       user_api: openmp
   internal_api: openmp
    num_threads: 32
       filepath: D:\my_test\sys\win64\python\Lib\site-packages\sklearn\.libs\vcomp140.dll
        version: None

       user_api: blas
   internal_api: openblas
    num_threads: 24
         prefix: libopenblas
       filepath: D:\my_test\sys\win64\python\Lib\site-packages\numpy.libs\libopenblas64__v0.3.23-293-gc2f4bdbb-gcc_10_3_0-2bde3a66a51006b2b53eb373ff767a3f.dll
        version: 0.3.23.dev
threading_layer: pthreads
   architecture: Prescott

       user_api: blas
   internal_api: openblas
    num_threads: 24
         prefix: libscipy_openblas
       filepath: D:\my_test\sys\win64\python\Lib\site-packages\scipy.libs\libscipy_openblas-f07f5a5d207a3a47104dca54d6d0c86a.dll
        version: 0.3.28
threading_layer: pthreads
   architecture: Haswell
```

COMMENT:
I think it shouldn't be an issue for the clonable estimator. Because the usage of callbacks are quite common. Maybe there there might be the option to reuse the synchronization primitives in all cloned estimators during the clone (deepcopy) operation. 

I overcome this by training estimators and then monkeypatch MultiOutputRegressor vars that are populated in fit() - then the export to onnx is working perfectly. So, the improvements might be to manage estimators manually, outside MultiOutputRegressor and provide them to MultiOutputRegressor in init or via the additional constructor 

Btw, predict() is also not working with not clonable estimator. 

not related to this issue: And I would add the capability to save/loade the model to MultiOutputRegressor. For example the save() method could save all estimators in a separate folder and zip into archive. That would be quite handy

Labels: Needs Investigation